package com.xlongwei.accounts.service;

import static com.xlongwei.accounts.controller.RecordController.fen;
import static com.xlongwei.accounts.controller.RecordController.yuan;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import org.noear.solon.Utils;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import org.noear.solon.data.annotation.Tran;
import org.smartboot.http.common.utils.NumberUtils;

import com.xlongwei.accounts.dao.AccountDao;
import com.xlongwei.accounts.dao.ExtraDao;
import com.xlongwei.accounts.dao.LedgerDao;
import com.xlongwei.accounts.dao.MemberDao;
import com.xlongwei.accounts.dao.RecordDao;
import com.xlongwei.accounts.model.Account;
import com.xlongwei.accounts.model.Extra;
import com.xlongwei.accounts.model.Ledger;
import com.xlongwei.accounts.model.Member;
import com.xlongwei.accounts.model.Record;
import com.xlongwei.accounts.view.Daily;
import com.xlongwei.accounts.view.Daily.Row;
import com.xlongwei.accounts.view.Daily.Row.RowBuilder;
import com.xlongwei.accounts.view.Overview;
import com.xlongwei.accounts.view.Query;
import com.xlongwei.accounts.view.Report;
import com.xlongwei.accounts.view.Yearly;
import com.xlongwei.accounts.view.Yearly.Monthly;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class RecordService {
        @Inject
        RecordDao recordDao;
        @Inject
        ExtraDao extraDao;
        @Inject
        LedgerDao ledgerDao;
        @Inject
        MemberDao memberDao;
        @Inject
        AccountDao accountDao;

        @Tran
        public Integer income(Integer userid, Integer ledgerid, Integer amount,
                        Integer accountid_in, Integer memberid, LocalDate day, String remark)
                        {
                log.info("{} income accountid_in={} amount={}", userid, accountid_in, amount);
                Record record = new Record();
                record.setUserid(userid);
                record.setLedgerid(ledgerid);
                record.setAmount(amount);
                record.setAccountidIn(accountid_in);
                record.setMemberid(memberid);
                record.setDay(day);
                Integer recordid = recordDao.create(record);
                log.info("recordid={} remark={}", recordid, remark);
                if (!Utils.isBlank(remark)) {
                        Extra extra = new Extra();
                        extra.setRecordid(recordid);
                        extra.setRemark(remark);
                        extraDao.create(extra);
                }
                return recordid;
        }

        @Tran
        public Integer outcome(Integer userid, Integer ledgerid, Integer amount,
                        Integer accountid_out, Integer memberid, LocalDate day, String remark)
                        {
                log.info("{} outcome accountid_out={} amount={}", userid, accountid_out, amount);
                Record record = new Record();
                record.setUserid(userid);
                record.setLedgerid(ledgerid);
                record.setAmount(amount);
                record.setAccountidOut(accountid_out);
                record.setMemberid(memberid);
                record.setDay(day);
                Integer recordid = recordDao.create(record);
                log.info("recordid={} remark={}", recordid, remark);
                if (!Utils.isBlank(remark)) {
                        Extra extra = new Extra();
                        extra.setRecordid(recordid);
                        extra.setRemark(remark);
                        extraDao.create(extra);
                }
                return recordid;
        }

        @Tran
        public Integer transfer(Integer userid, Integer ledgerid, Integer amount,
                        Integer accountid_out, Integer accountid_in, Integer memberid, LocalDate day, String remark)
                        {
                log.info("{} transfer accountid_out={} accountid_in={} amount={}", userid,
                                accountid_out, accountid_in, amount);
                Record record = new Record();
                record.setUserid(userid);
                record.setLedgerid(ledgerid);
                record.setAmount(amount);
                record.setAccountidIn(accountid_in);
                record.setAccountidOut(accountid_out);
                record.setMemberid(memberid);
                record.setDay(day);
                Integer recordid = recordDao.create(record);
                log.info("recordid={} remark={}", recordid, remark);
                if (!Utils.isBlank(remark)) {
                        Extra extra = new Extra();
                        extra.setRecordid(recordid);
                        extra.setRemark(remark);
                        extraDao.create(extra);
                }
                return recordid;
        }

        // 充值+红包
        @Tran
        public Integer charge(Integer userid, Integer amount, Integer income, Integer accountid_out,
                        Integer accountid_in, Integer memberid, LocalDate day, String remark) {
                log.info("{} charge accountid_out={} accountid_in={} amount={}", userid,
                                accountid_out, accountid_in, amount);
                Integer recordid = transfer(userid, 4, amount, accountid_out, accountid_in, memberid, day,
                                income > 0 ? null : remark);
                if (income > 0) {
                        income(userid, 12, income, accountid_in, memberid, day, remark);
                }
                return recordid;
        }

        // 还款+利息
        @Tran
        public Integer repay(Integer userid, Integer amount, Integer outcome, Integer accountid_out,
                        Integer accountid_in, Integer memberid, LocalDate day, String remark) {
                log.info("{} repay accountid_out={} accountid_in={} amount={}", userid,
                                accountid_out, accountid_in, amount);
                Integer recordid = transfer(userid, 5, amount, accountid_out, accountid_in, memberid, day,
                                outcome > 0 ? null : remark);
                if (outcome > 0) {
                        outcome(userid, 2, outcome, accountid_out, memberid, day, remark==null||remark.isEmpty() ? "利息" : remark);
                }
                return recordid;
        }
        
        public Record retrieve(Integer recordid) {
                return recordDao.retrieve(recordid);
        }

        @Tran
        public Integer update(Record record, Integer ledgerid, Integer amount,
                        Integer accountid_out, Integer accountid_in, Integer memberid,
                        LocalDate day, String remark) {
                Integer recordid = record.getRecordid();
                record.setLedgerid(Optional.ofNullable(ledgerid).orElse(record.getLedgerid()));
                record.setAmount(Optional.ofNullable(amount).orElse(record.getAmount()));
                record.setAccountidIn(Optional.ofNullable(accountid_in).orElse(record.getAccountidIn()));
                record.setAccountidOut(Optional.ofNullable(accountid_out).orElse(record.getAccountidOut()));
                record.setMemberid(Optional.ofNullable(memberid).orElse(record.getMemberid()));
                record.setDay(Optional.ofNullable(day).orElse(record.getDay()));
                log.info("update record {}", record);
                Integer update = recordDao.update(record);
                Extra extra = extraDao.retrive(recordid);
                if ((extra == null && !Utils.isBlank(remark))
                                || (extra != null && !Objects.equals(remark, extra.getRemark()))) {
                        log.info("recordid={} remark={}", recordid, remark);
                        if (extra == null) {
                                extra = new Extra();
                                extra.setRecordid(recordid);
                                extra.setRemark(remark);
                                extraDao.create(extra);
                        } else {
                                extra.setRemark(remark);
                                extraDao.update(extra);
                        }
                }
                return update;
        }

        @SuppressWarnings("rawtypes")
        public Overview report(Report report) {
                Overview overview = new Overview();
                List<Map> list = recordDao.report(report);
                List<Ledger> ledgers = report.getUserids().stream().flatMap(userid -> ledgerDao.list(userid).stream()).collect(Collectors.toList());
                List<Account> accounts = report.getUserids().stream().flatMap(userid -> accountDao.list(userid).stream()).collect(Collectors.toList());
                List<Member> members = report.getUserids().stream().flatMap(userid -> memberDao.list(userid).stream()).collect(Collectors.toList());
                Map<String, Yearly> years = new LinkedHashMap<>();
                for (Map map : list) {
                        Object yearMonth = map.get("year_month");
                        String year_month = yearMonth instanceof byte[] ? new String((byte[])yearMonth) : yearMonth.toString();
                        long sum_in = Long.parseLong(map.get("sum_in").toString());
                        long sum_out = Long.parseLong(map.get("sum_out").toString());
                        long transfers = report.isCountRecords() ? Long.parseLong(map.get("transfers").toString()) : 0;
                        int ledgerid = NumberUtils.toInt(String.valueOf(map.get("ledgerid")), 0);
                        int accountid = NumberUtils.toInt(String.valueOf(map.get("accountid")), 0);
                        int memberid = NumberUtils.toInt(String.valueOf(map.get("memberid")), 0);
                        int userid = NumberUtils.toInt(String.valueOf(map.get("userid")), 0);
                        Yearly yearly = years.computeIfAbsent(year_month.substring(0, 4), key -> new Yearly());
                        Monthly monthly = yearly.getMonths().computeIfAbsent(year_month, key -> new Monthly());
                        if("account".equals(report.getGroupBy())){
                                if(sum_in>0)monthly.id_incomes(accountid).addAndGet(sum_in);
                                if(sum_out>0)monthly.id_outcomes(accountid).addAndGet(sum_out);
                        }else if("ledger".equals(report.getGroupBy())){
                                if(sum_in>0)monthly.id_incomes(ledgerid).addAndGet(sum_in);
                                if(sum_out>0)monthly.id_outcomes(ledgerid).addAndGet(sum_out);
                        }else if("member".equals(report.getGroupBy())){
                                if(sum_in>0)monthly.id_incomes(memberid).addAndGet(sum_in);
                                if(sum_out>0)monthly.id_outcomes(memberid).addAndGet(sum_out);
                        }else if("user".equals(report.getGroupBy())){
                                if(sum_in>0)monthly.id_incomes(userid).addAndGet(sum_in);
                                if(sum_out>0)monthly.id_outcomes(userid).addAndGet(sum_out);
                        }
                        monthly.getIncomes().addAndGet(sum_in);
                        monthly.getOutcomes().addAndGet(sum_out);
                        monthly.setNet_assets(yuan(transfers));
                }
                long net_assets = 0, year_transfers = 0;
                for (Entry<String, Yearly> yearlyEntry : reverse(years.entrySet())) {
                        Yearly yearly = yearlyEntry.getValue();
                        yearly.setYear(yearlyEntry.getKey());
                        year_transfers = 0; // 不同于净值一直累加，年转账流水每年重新计算
                        for (Entry<String, Monthly> monthlyEntry : reverse(yearly.getMonths().entrySet())) {
                                Monthly monthly = monthlyEntry.getValue();
                                monthly.setMonth(monthlyEntry.getKey());
                                monthly.setSum_in(yuan(monthly.getIncomes().get()));
                                monthly.setSum_out(yuan(monthly.getOutcomes().get()));
                                if (report.isCountRecords()) {
                                        long transfers = (long)fen(monthly.getNet_assets());
                                        net_assets += transfers; // 借用net_assets计算总转账流水
                                        year_transfers += transfers;
                                } else {
                                        net_assets = net_assets + monthly.getIncomes().get() - monthly.getOutcomes().get();
                                        monthly.setNet_assets(yuan(net_assets));
                                }
                                yearly.getIncomes().addAndGet(monthly.getIncomes().get());
                                yearly.getOutcomes().addAndGet(monthly.getOutcomes().get());
                                if (!Utils.isBlank(report.getGroupBy())) {
                                        for (Entry<Integer, AtomicLong> entry : sorted(
                                                        monthly.getId_incomes().entrySet())) {
                                                Integer id = entry.getKey();
                                                String name = "account".equals(report.getGroupBy()) ? accountName(accounts, id)
                                                                : ("ledger".equals(report.getGroupBy()) ? ledgerName(ledgers, id)
                                                                : ("member".equals(report.getGroupBy()) ? memberName(members, id):(id==report.getUserids().get(0) ? "自己" : "爱人")));
                                                monthly.getSum_ins().put(name, yuan(entry.getValue().get()));
                                                yearly.name_incomes(name).addAndGet(entry.getValue().get());
                                        }
                                        for (Entry<Integer, AtomicLong> entry : sorted(monthly.getId_outcomes()
                                                        .entrySet())) {
                                                Integer id = entry.getKey();
                                                String name = "account".equals(report.getGroupBy()) ? accountName(accounts, id)
                                                                : ("ledger".equals(report.getGroupBy()) ? ledgerName(ledgers, id)
                                                                : ("member".equals(report.getGroupBy()) ? memberName(members, id):(id==report.getUserids().get(0) ? "自己" : "爱人")));
                                                monthly.getSum_outs().put(name, yuan(entry.getValue().get()));
                                                yearly.name_outcomes(name).addAndGet(entry.getValue().get());
                                        }
                                } else {
                                        monthly.setSum_ins(null);
                                        monthly.setSum_outs(null);
                                }
                        }
                        yearly.setSum_in(yuan(yearly.getIncomes().get()));
                        yearly.setSum_out(yuan(yearly.getOutcomes().get()));
                        yearly.setNet_assets(yuan(report.isCountRecords() ? year_transfers : net_assets));
                        overview.getIncomes().addAndGet(yearly.getIncomes().get());
                        overview.getOutcomes().addAndGet(yearly.getOutcomes().get());
                        if (!Utils.isBlank(report.getGroupBy())) {
                                for (Entry<String, AtomicLong> entry : sorted(yearly.getName_incomes().entrySet())) {
                                        yearly.getSum_ins().put(entry.getKey(), yuan(entry.getValue().get()));
                                        overview.name_incomes(entry.getKey()).addAndGet(entry.getValue().get());
                                }
                                for (Entry<String, AtomicLong> entry : sorted(
                                                yearly.getName_outcomes().entrySet())) {
                                        yearly.getSum_outs().put(entry.getKey(), yuan(entry.getValue().get()));
                                        overview.name_outcomes(entry.getKey()).addAndGet(entry.getValue().get());
                                }
                        } else {
                                yearly.setSum_ins(null);
                                yearly.setSum_outs(null);
                        }
                        yearly.setRecords(new ArrayList<>(yearly.getMonths().values()));
                }
                overview.setSum_in(yuan(overview.getIncomes().get()));
                overview.setSum_out(yuan(overview.getOutcomes().get()));
                overview.setNet_assets(yuan(net_assets));
                if (!Utils.isBlank(report.getGroupBy())) {
                        for (Entry<String, AtomicLong> entry : sorted(overview.getName_incomes().entrySet())) {
                                overview.getSum_ins().put(entry.getKey(), yuan(entry.getValue().get()));
                        }
                        for (Entry<String, AtomicLong> entry : sorted(overview.getName_outcomes().entrySet())) {
                                overview.getSum_outs().put(entry.getKey(), yuan(entry.getValue().get()));
                        }
                } else {
                        overview.setSum_ins(null);
                        overview.setSum_outs(null);
                }
                overview.setRecords(new ArrayList<>(years.values()));
                return overview;
        }

        private <T> List<Entry<String, T>> reverse(Set<Entry<String, T>> set) {
                List<Entry<String, T>> list = new ArrayList<>(set);
                Collections.reverse(list);
                return list;
        }

        private <T> List<Entry<T, AtomicLong>> sorted(Set<Entry<T, AtomicLong>> set) {
                List<Entry<T, AtomicLong>> list = new ArrayList<>(set);
                list.sort((entry1, entry2) -> {
                        return (int)(entry2.getValue().get() - entry1.getValue().get());
                });
                return list;
        }

        public List<Daily> list(Query query) {
                List<Record> list = recordDao.list(query);
                List<Ledger> ledgers = ledgerDao.list(query.getUserid());
                List<Account> accounts = accountDao.list(query.getUserid());
                List<Member> members = memberDao.list(query.getUserid());
                Map<String, Daily> map = new LinkedHashMap<>();
                for (Record record : list) {
                        String day = record.getDay().toString();
                        Daily daily = map.computeIfAbsent(day, (key) -> new Daily());
                        RowBuilder row = Row.builder().recordid(record.getRecordid())
                                        .fen(record.getAmount())
                                        .accountid_in(record.getAccountidIn())
                                        .accountid_out(record.getAccountidOut())
                                        .ledgerid(record.getLedgerid())
                                        .memberid(record.getMemberid())
                                        .ledger(ledgerName(ledgers, record.getLedgerid()))
                                        .amount(yuan(record.getAmount()))
                                        .member(memberName(members, record.getMemberid()))
                                        .remark(ExtraDao.toUnicode(record.getRemark()))
                                        .userid(record.getUserid());
                        if (record.isIncome()) {// income
                                daily.getIncomes().addAndGet(record.getAmount());
                                row.account(accountName( accounts, record.getAccountidIn()));
                        } else if (record.isOutcome()) {// outcome
                                daily.getOutcomes().addAndGet(record.getAmount());
                                row.account(accountName( accounts, record.getAccountidOut()));
                        } else if (record.isTransfer()) {// transfer
                                row.account(accountName( accounts, record.getAccountidOut()));
                                row.account2(accountName( accounts, record.getAccountidIn()));
                        }
                        daily.getRecords().add(row.build());
                }
                map.forEach((day, daily) -> {
                        daily.setDay(day);
                        daily.setSum_in(yuan(daily.getIncomes().get()));
                        daily.setSum_out(yuan(daily.getOutcomes().get()));
                });
                return new ArrayList<>(map.values());
        }

        private String ledgerName(List<Ledger> ledgers, Integer ledgerid) {
                Ledger ledger = ledgers.stream().filter(item -> ledgerid.equals(item.getLedgerid())).findFirst().orElse(null);
                return ledger == null ? ledgerid.toString() : ledger.getName();
        }

        private String accountName(List<Account> accounts, Integer accountid) {
                Account account = accounts.stream().filter(item -> accountid.equals(item.getAccountid())).findFirst().orElse(null);
                return account == null ? accountid.toString() : account.getName();
        }

        private String memberName(List<Member> members, Integer memberid) {
                Member member = members.stream().filter(item -> memberid.equals(item.getMemberid())).findFirst().orElse(null);
                return member == null ? memberid.toString() : member.getName();
        }
}
